﻿using System.Security.Claims;
using System.Web;
using Devonline.AspNetCore;
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Devonline.Identity;

/// <summary>
/// 授权登录服务
/// </summary>
public class AuthenticationService(
    UserManager<User> userManager,
    SignInManager<User> signInManager,
    HttpSetting httpSetting,
    IdentityDbContext context,
    ILogger<AuthenticationService> logger,
    IDataService<User> userService,
    IDataService<OAuthUser> oauthUserService,
    IHttpContextAccessor httpContextAccessor
    )
{
    private readonly UserManager<User> _userManager = userManager;
    private readonly SignInManager<User> _signInManager = signInManager;
    private readonly HttpSetting _httpSetting = httpSetting;
    private readonly IdentityDbContext _context = context;
    private readonly ILogger<AuthenticationService> _logger = logger;
    private readonly IDataService<User> _userService = userService;
    private readonly IDataService<OAuthUser> _oauthUserService = oauthUserService;
    private readonly HttpContext _httpContext = httpContextAccessor.HttpContext!;

    /// <summary>
    /// Post processing of external authentication
    /// 外部登录更关心认证用户
    /// </summary>
    public async Task<User> LoginAsync()
    {
        // read external identity from the temporary cookie
        var scheme = CookieAuthenticationDefaults.AuthenticationScheme;
        var result = await _httpContext.AuthenticateAsync(scheme);
        if (!(result?.Succeeded ?? false))
        {
            throw new Exception("External authentication error");
        }

        var user = await AutoProvisionUserFromExternalProviderAsync(result);
        var claims = await GetUserClaimsAsync(user);

        _logger.LogDebug("user {user} will sign in with claims!", user.UserName);
        result.Properties.IsPersistent = false;
        await _signInManager.SignInWithClaimsAsync(user, result.Properties, claims);
        await _httpContext.SignOutAsync(scheme);
        return user;
    }

    /// <summary>
    /// 根据用户获取重定向的地址
    /// </summary>
    /// <param name="user">当前登录用户</param>
    /// <param name="redirectUrl">重定向地址</param>
    /// <returns></returns>
    public string GetRedirectUrl(User user, string? redirectUrl = default)
    {
        redirectUrl ??= _httpContext.GetClaimValue<string>(AppSettings.CLAIM_TYPE_REDIRECT_URL) ?? _httpContext.GetClaimValue<string>(AppSettings.CLAIM_TYPE_REDIRECT_URI);
        if (string.IsNullOrWhiteSpace(redirectUrl) && _httpContext.Items.TryGetValue(nameof(AuthenticationProperties.RedirectUri), out object? value) && value is not null)
        {
            redirectUrl = value.ToString();
        }

        redirectUrl ??= AppSettings.CHAR_SLASH.ToString();
        if (user.PhoneNumber is null || string.IsNullOrEmpty(user.PhoneNumber) || (user.PhoneNumber is not null && !user.PhoneNumberConfirmed))
        {
            redirectUrl = _httpSetting.UserInteraction!.BindPhoneNumber + $"?{AppSettings.CLAIM_TYPE_REDIRECT_URL}={redirectUrl}";
        }

        return HttpUtility.UrlDecode(redirectUrl);
    }
    /// <summary>
    /// 增加用户自定义 claims
    /// </summary>
    /// <param name="user"></param>
    /// <returns></returns>
    public async Task<List<Claim>> GetUserClaimsAsync(User user)
    {
        ArgumentNullException.ThrowIfNull(user.Name);
        ArgumentNullException.ThrowIfNull(user.UserName);

        var claims = new List<Claim>{
            new(JwtClaimTypes.Name, user.Name),
            new(AppSettings. CLAIM_TYPE_USER_ID, user.Id),
            new(AppSettings.CLAIM_TYPE_USER_NAME, user.UserName),
        };

        if (!string.IsNullOrWhiteSpace(user.Alias))
        {
            claims.Add(new Claim(nameof(Identity.User.Alias), user.Alias));
        }

        if (!string.IsNullOrWhiteSpace(user.Image))
        {
            claims.Add(new Claim(nameof(Identity.User.Image), user.Image));
        }

        if (!string.IsNullOrWhiteSpace(user.GroupId))
        {
            claims.Add(new Claim(AppSettings.CLAIM_TYPE_GROUP_ID, user.GroupId));
        }

        if (_httpSetting.DataIsolate != DataIsolateLevel.None && user.Type < AuthorizeType.Authorizer)
        {
            switch (_httpSetting.DataIsolate)
            {
                case DataIsolateLevel.Individual:
                    claims.Add(new Claim(AppSettings.CLAIM_TYPE_DATA_ISOLATE_ID, user.Id));
                    break;
                case DataIsolateLevel.Subordinate:
                    if (!string.IsNullOrWhiteSpace(user.GroupId))
                    {
                        claims.Add(new Claim(AppSettings.CLAIM_TYPE_DATA_ISOLATE_ID, user.GroupId));
                    }
                    break;
                case DataIsolateLevel.TopGroup:
                    if (!string.IsNullOrWhiteSpace(user.GroupId))
                    {
                        var topGroup = await _context.GetTopParentAsync<Group>(user.GroupId);
                        if (topGroup is not null)
                        {
                            claims.Add(new Claim(AppSettings.CLAIM_TYPE_DATA_ISOLATE_ID, topGroup.Id));
                        }
                    }
                    break;
                default:
                    break;
            }
        }

        return claims;
    }

    /// <summary>
    /// 根据第三方授权返回情况, 创建 oauth 用户和 系统用户
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    public async Task<User> AutoProvisionUserFromExternalProviderAsync(AuthenticateResult result)
    {
        var principal = result.Principal ?? _httpContext.User;
        ArgumentNullException.ThrowIfNull(principal);
        // try to determine the unique id of the external user (issued by the provider)
        // the most common claim type for that are the sub claim and the NameIdentifier
        // depending on the external provider, some other claim type might be used
        var userIdClaim = principal.FindFirst(JwtClaimTypes.Subject) ?? principal.FindFirst(ClaimTypes.NameIdentifier) ?? throw new Exception("Unknown userid");

        // remove the user id claim so we don't include it as an extra claim if/when we provision the user
        var claims = principal.Claims.ToList();
        claims.Remove(userIdClaim);

        var provider = result.Properties?.Items["scheme"];
        ArgumentNullException.ThrowIfNull(provider);

        var providerUserId = userIdClaim.Value;
        var openId = claims.GetClaimValue(AppSettings.CLAIM_TYPE_OPEN_ID, provider) ?? providerUserId;
        var phoneNumber = claims.GetClaimValue(AppSettings.CLAIM_TYPE_PHONE_NUMBER, provider) ?? claims.GetClaimValue(JwtClaimTypes.PhoneNumber, provider) ?? claims.GetClaimValue(ClaimTypes.MobilePhone) ?? claims.GetClaimValue(ClaimTypes.HomePhone) ?? claims.GetClaimValue(ClaimTypes.OtherPhone);

        // find external user
        var oauthUser = await _oauthUserService.FirstOrDefaultAsync(x => x.OpenId == openId);
        var user = await _userManager.FindByLoginAsync(provider, providerUserId);
        if (phoneNumber is not null)
        {
            user ??= await _userService.FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);
        }

        if (oauthUser is not null)
        {
            if (user is null && oauthUser.UserId is not null)
            {
                user = await _userService.FirstOrDefaultAsync(x => x.Id == oauthUser.UserId);
            }

            if (user is not null)
            {
                user.PasswordHash = null;
                user.PhoneNumber = user.PhoneNumber.Desensitize();
                return user;
            }
        }

        var name = claims.GetClaimValue(JwtClaimTypes.Name, provider) ?? claims.GetClaimValue(ClaimTypes.Name) ?? claims.GetClaimValue("nickname", provider) ?? claims.GetClaimValue(AppSettings.CLAIM_TYPE_ALIAS);
        var headImgUrl = claims.GetClaimValue(AppSettings.CLAIM_TYPE_HEAD_IMG_URL, provider) ?? claims.GetClaimValue(AppSettings.CLAIM_TYPE_HEAD_IMAGE);
        var gender = claims.GetClaimValue(JwtClaimTypes.Gender, provider);
        var email = claims.GetClaimValue(JwtClaimTypes.Email, provider) ?? claims.GetClaimValue(ClaimTypes.Email);

        ArgumentNullException.ThrowIfNull(name);
        ArgumentNullException.ThrowIfNull(openId);

        oauthUser = await _oauthUserService.AddAsync(new OAuthUser
        {
            Id = openId,
            Type = AuthorizeType.ThirdParty,
            Name = name,
            Alias = name,

            OpenId = openId,
            AuthType = GetAuthTypeByProvider(provider),
            UserId = user?.Id,

            Image = headImgUrl,
            Gender = string.IsNullOrEmpty(gender) ? Gender.Unknown : (Gender)Enum.Parse(typeof(Gender), gender),
            Description = name + AppSettings.CHAR_AT + provider,
            UnionId = claims.GetClaimValue("unionid", provider),
            Privileges = claims.GetClaimValue("privilege", provider)
        });

        if (user is null)
        {
            //用户尚不存在时
            user = new User
            {
                Name = name,
                Alias = name,
                UserName = openId,
                PhoneNumber = phoneNumber,
                Email = email,
                Image = headImgUrl,
                Type = AuthorizeType.ThirdParty,
                Description = oauthUser.Description
            };

            user.Create();
            user.Update();
            var identityResult = await _userManager.CreateAsync(user);
            if (!identityResult.Succeeded)
            {
                throw new Exception(identityResult.Errors.First().Description);
            }

            claims.Add(new Claim(AppSettings.CLAIM_TYPE_USER_NAME, openId));
            identityResult = await _userManager.AddClaimsAsync(user, claims);
            if (!identityResult.Succeeded)
            {
                throw new Exception(identityResult.Errors.First().Description);
            }

            identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider));
            if (!identityResult.Succeeded)
            {
                throw new Exception(identityResult.Errors.First().Description);
            }

            if (string.IsNullOrWhiteSpace(oauthUser.UserId))
            {
                oauthUser.UserId = user.Id;
                await _oauthUserService.UpdateAsync(oauthUser);
            }
        }

        user.PasswordHash = null;
        user.PhoneNumber = user.PhoneNumber.Desensitize();
        return user;
    }
    /// <summary>
    /// if the external login is OIDC-based, there are certain things we need to preserve to make logout work, this will be different for WS-Fed, SAML2p or other protocols
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    public static AuthenticationProperties ProcessLoginCallback(AuthenticateResult result)
    {
        // this allows us to collect any additional claims or properties
        // for the specific protocols used and store them in the local auth cookie.
        // this is typically used to store data needed for signout from those protocols.

        // if the external provider issued an id_token, we'll keep it for signout
        var properties = new AuthenticationProperties();
        var idToken = result.Properties?.GetTokenValue("id_token");
        if (idToken is not null)
        {
            properties.StoreTokens([new AuthenticationToken { Name = "id_token", Value = idToken }]);
        }

        return properties;
    }

    /// <summary>
    /// 获取 cookie 类型的 scheme
    /// </summary>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    private async Task<string> GetCookieAuthenticationSchemeAsync()
    {
        var schemes = _httpContext.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
        var scheme = await schemes.GetDefaultAuthenticateSchemeAsync() ?? throw new InvalidOperationException("No DefaultAuthenticateScheme found or no CookieAuthenticationScheme configured on IdentityServerOptions.");
        return scheme.Name;
    }
    /// <summary>
    /// 根据 provider 获取认证方式
    /// </summary>
    /// <param name="provider"></param>
    /// <returns></returns>
    private AuthType GetAuthTypeByProvider(string provider)
    {
        try
        {
            var authType = Enum.Parse<AuthType>(provider);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "根据 provider 获取认证方式失败, 将按 OAuth2 作为默认情况处理!");
        }

        return AuthType.OAuth2;
    }
}